Conversation
Replace references to docker-compose.staging.yml with docker-compose.yml in .github/workflows/docker-build-staging.yml. This ensures the staging CI uses the generic compose file for both the docker-compose and docker compose command paths, simplifying configuration and avoiding filename mismatches.
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (3)
WalkthroughThis PR removes the v1 Auth, Admin, API, and MCP modules and services, and introduces a new McpV2 module: controller, services (McpV2Service, ApiClientV2Service, SessionStoreService), tool definitions, and updates AppModule and startup logging accordingly. Changes
Sequence DiagramsequenceDiagram
participant Client
participant McpV2Controller
participant SessionStoreService
participant McpV2Service
participant ApiClientV2Service
participant Redis
Client->>McpV2Controller: HTTP /mcp (mcp-session-id?, x-api-key?)
McpV2Controller->>SessionStoreService: getApiKey(sessionId)
SessionStoreService->>Redis: GET mcp_v2:session:{id}
Redis-->>SessionStoreService: apiKey or null
alt No stored apiKey
McpV2Controller->>SessionStoreService: setApiKey(sessionId, x-api-key)
SessionStoreService->>Redis: SETEX mcp_v2:session:{id} (TTL)
Redis-->>SessionStoreService: OK
end
McpV2Controller->>McpV2Service: createServer() / route request to tool
McpV2Service->>ApiClientV2Service: HTTP call (uses resolved apiKey)
ApiClientV2Service-->>McpV2Service: API response
McpV2Service-->>McpV2Controller: Tool result
McpV2Controller->>Client: JSON-RPC response
Note over McpV2Controller,SessionStoreService: On transport close -> SessionStoreService.delete(sessionId) -> Redis DEL
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main.ts (1)
36-60:⚠️ Potential issue | 🟡 MinorSwagger auth definitions reference removed modules.
addBearerAuth(Firebase JWT) andaddApiKey(x-admin-api-key) are still configured, but this PR removes the Auth and Admin modules. The new MCP v2 controller usesx-api-keyinstead. These stale Swagger security schemes will confuse API consumers.Consider updating these to reflect the actual
x-api-keyheader used byMcpV2Controller, or removing them if Swagger is excluded for the MCP endpoint (which uses@ApiExcludeEndpoint()).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main.ts` around lines 36 - 60, The Swagger security schemes are stale: remove the call to DocumentBuilder.addBearerAuth(...) and the addApiKey(...) that registers 'x-admin-api-key', and replace them with a single addApiKey(...) entry for the actual header used by McpV2Controller (name: 'x-api-key', in: 'header', description: 'API key for MCP v2 access') or drop all auth definitions entirely if the MCP endpoints are excluded from Swagger via `@ApiExcludeEndpoint`() — update the DocumentBuilder usage accordingly to reflect only the current x-api-key scheme.
🧹 Nitpick comments (6)
src/mcp_v2/controllers/mcp-v2.controller.ts (3)
44-44: Unnecessary non-null assertion onapiKey.
apiKeyis already validated as truthy on Line 33 (the function returns early if falsy). The!assertion on Line 44 is redundant.Suggested fix
- await this.sessionStore.setApiKey(newSessionId, apiKey!); + await this.sessionStore.setApiKey(newSessionId, apiKey);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/mcp_v2/controllers/mcp-v2.controller.ts` at line 44, The non-null assertion on apiKey is redundant; update the controller code that calls this.sessionStore.setApiKey(newSessionId, apiKey!) to pass apiKey directly (remove the trailing '!') since apiKey is already checked earlier, e.g., in the method where newSessionId and apiKey are used; keep the call as this.sessionStore.setApiKey(newSessionId, apiKey) so the types remain consistent and no needless assertion is present.
17-20:@ApiExcludeEndpoint()overrides the other Swagger decorators.
@ApiExcludeEndpoint()removes this endpoint from the generated Swagger document, making@ApiOperationand@ApiResponseon Lines 18-19 dead code. Either remove the Swagger decorators or remove@ApiExcludeEndpoint()depending on the intent.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/mcp_v2/controllers/mcp-v2.controller.ts` around lines 17 - 20, The decorators on the MCP v2 controller are contradictory: `@ApiExcludeEndpoint`() hides the endpoint from Swagger while `@ApiOperation` and `@ApiResponse` on the same handler are therefore ineffective; decide the intent and make them consistent—if you want the endpoint documented, remove `@ApiExcludeEndpoint`() from the controller (retain `@ApiOperation` and `@ApiResponse`), otherwise remove the `@ApiOperation` and `@ApiResponse` decorators (keep `@ApiExcludeEndpoint`) so there are no dead annotations; look for the decorators on the method where `@All`(), `@ApiOperation`, `@ApiResponse`, and `@ApiExcludeEndpoint` are applied to update accordingly.
29-63: No horizontal scaling: session state is process-local.The
transportsMap is local to this controller instance. In a multi-instance deployment, a returning client with a validmcp-session-idcould hit a different instance that doesn't have the transport, causing it to fall into the "new session" path and fail withMissing x-api-key.If multi-instance deployment is planned, consider sticky sessions (e.g., via load balancer affinity on
mcp-session-id) or document this as a single-instance constraint.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/mcp_v2/controllers/mcp-v2.controller.ts` around lines 29 - 63, The transports Map (this.transports) is process-local so a returning sessionId may not be found on other instances; update the handler to avoid silently treating a missing transport as a "new session" and either (A) require/declare sticky sessions: when sessionId is present but this.transports.has(sessionId) is false, respond with a clear 410/400 JSON-RPC error referencing the missing session and instruct the client/operator to use load-balancer affinity on mcp-session-id, or (B) implement a distributed session-to-instance registry (reuse sessionStore to persist the owning instance id when onsessioninitialized in StreamableHTTPServerTransport and, on a missing transport in transport lookup, proxy/forward the request to the recorded instance or return a redirect/error). Modify the code paths around sessionId lookup, the transport creation block, and onsessioninitialized/transport.onclose to set and remove the instance id in the shared sessionStore so other instances can detect and handle missing transports.src/main.ts (1)
12-21:enableCorsis computed but never used — CORS is always enabled.Line 13 computes
enableCorsfrom the config, but it's never checked beforeapp.enableCors(...)on Line 16. CORS is unconditionally applied regardless of theENABLE_CORSsetting.Suggested fix
Either remove the dead variable (as the comment says "always enable for MCP compatibility") or actually gate the call:
- const enableCors = configService.get<string>('ENABLE_CORS') !== 'false'; - const origins = configService.get<string>('CORS_ORIGINS')?.split(',') || ['*']; - - app.enableCors({ + const origins = configService.get<string>('CORS_ORIGINS')?.split(',') || ['*']; + app.enableCors({🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main.ts` around lines 12 - 21, The variable enableCors is computed but never used, so update the logic around app.enableCors: either remove the unused enableCors variable and its comment if CORS should always be enabled, or use enableCors to gate the call to app.enableCors (e.g. if (!enableCors) skip calling app.enableCors). Locate the computation of enableCors and the app.enableCors(...) invocation in main.ts and implement the chosen option so the ENABLE_CORS config actually controls whether app.enableCors is invoked.src/mcp_v2/services/mcp-v2.service.ts (1)
89-115: Magic command numbers reduce readability.The numeric command codes (1, 28, 29, 20, 17) are hardcoded without explanation. Consider extracting them as named constants for maintainability:
Example
const COMMAND_CODES = { SWITCH: 1, BRIGHTNESS: 28, KELVIN: 29, TEMPERATURE: 20, MODE: 17, } as const;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/mcp_v2/services/mcp-v2.service.ts` around lines 89 - 115, Replace the magic numeric command codes used when building the command array for args.action by extracting them into clearly named constants (e.g., SWITCH, BRIGHTNESS, KELVIN, TEMPERATURE, MODE) and then use those constants when constructing the command arrays (replace occurrences of 1, 28, 29, 20, 17 with the corresponding named constants); update the switch handling of args.action and the command variable to reference these constants so the intent is clear and maintainability improved.src/mcp_v2/mcp-v2.module.ts (1)
9-14: Module wiring looks clean overall.Two minor observations:
ConfigModuleis alreadyisGlobal: trueinAppModule(seesrc/app.module.tsLine 12), so the import here is redundant (harmless, but unnecessary).HttpModuleis imported bare here, whileAppModuleregisters it withtimeout: 30000andmaxRedirects: 5. This meansApiClientV2Servicegets an unconfiguredHttpServiceinstance. This is currently fine becauseApiClientV2Servicesets its own per-request timeout, but it's worth being aware of if you later rely on module-level Axios defaults.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/mcp_v2/mcp-v2.module.ts` around lines 9 - 14, Remove the redundant import of ConfigModule from the MCP V2 module since it is already globally imported in AppModule. Additionally, consider importing HttpModule with the same configuration used in AppModule (timeout and maxRedirects) to ensure consistent HttpService settings for ApiClientV2Service, especially if default Axios settings might be relied on later.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/mcp_v2/controllers/mcp-v2.controller.ts`:
- Line 12: The in-memory transports Map (transports: Map<string,
StreamableHTTPServerTransport>) can leak if sessions never trigger onclose; add
a periodic cleanup task that sweeps the Map and evicts stale entries: for each
key, check the session TTL/state in Redis (the same session key used by
McpServer) and remove/close the StreamableHTTPServerTransport and its McpServer
when the Redis TTL indicates expiry or the session no longer exists, or
alternatively attach a max-age timestamp to each Map entry and remove entries
older than that age; ensure cleanup runs on a timer (e.g., setInterval) and
properly calls transport.close()/server.shutdown() to release resources.
In `@src/mcp_v2/services/api-client-v2.service.ts`:
- Around line 57-61: The catch block in ApiClientV2Service currently throws
axiosError.response?.data or the raw axiosError, which loses HTTP status and
original stack; update the catch in the method handling requests (the catch that
casts to AxiosError) to construct and throw a structured error object (or a
custom Error subclass) that includes status: axiosError.response?.status,
message: axiosError.response?.data?.message || axiosError.message, context: {
method, path }, details: axiosError.response?.data, and preserve the original
error via an originalError or cause property so the stack trace remains
available; ensure callers in McpV2Service can inspect .status and .details
instead of relying on response data alone.
In `@src/mcp_v2/services/mcp-v2.service.ts`:
- Around line 28-41: Extract the repeated userId resolution into a private
helper (e.g., resolveUserIdFromFindApi) that accepts apiKey and payload/data,
calls this.apiClient.post('/api/v2.0/iot-core/user/findUserId', apiKey, { data
}), and returns the normalized userId by applying the fallback chain
(resp?.userId || resp?.user_id || resp?.data?.userId) or throws a clear error;
then replace the inline logic inside each server.registerTool handlers
(ToolsListV2.find_user_id and the other two handlers) to call
this.resolveUserIdFromFindApi(sessionApiKey, data) after obtaining sessionId and
apiKey from this.sessionStore.getApiKey, and return the existing { content:
[...] } using the helper result.
In `@src/mcp_v2/services/session-store.service.ts`:
- Around line 31-33: Guard the onModuleDestroy method so it safely handles a
partially-initialized Redis client: check that this.client exists and is in a
state that supports quit (e.g., non-null and not already closed) before calling
this.client.quit(), and wrap the quit call in a try/catch to swallow or log any
errors; update the onModuleDestroy implementation to reference the existing
onModuleInit/client initialization flow and ensure no unguarded .quit() calls
occur if onModuleInit failed.
- Around line 22-27: The Redis client initialization in the constructor uses
this.client = new Redis(...) and only one-time 'ready'/'error' listeners, so it
needs a retryStrategy, maxRetriesPerRequest and a persistent error listener to
avoid unbounded buffering and unhandled post-init errors; update the Redis
options passed to new Redis(...) to include a retryStrategy (mirror the logic
from src/mcp/services/redis.service.ts), set maxRetriesPerRequest to a sane
value (and consider enableOfflineQueue=false if appropriate), and replace the
one-time 'error' listener with a persistent this.client.on('error', handler)
that logs/handles errors after init while keeping the existing init Promise
resolution using once('ready')/once('error').
In `@src/mcp_v2/tools/tools-list-v2.ts`:
- Around line 13-16: The list_devices inputSchema is missing the data field that
the handler in mcp-v2.service.ts destructures as { userId, data }, so add a data
property to the list_devices inputSchema (alongside userId) that accepts the
shape used by the handler (e.g., an object with email and/or phone fields) so
clients can send fallback lookup info and the SDK validation won't strip it;
update the z.object for list_devices.inputSchema to include data with
appropriate z types/description to match the email/phone resolution logic in the
list_devices handler.
---
Outside diff comments:
In `@src/main.ts`:
- Around line 36-60: The Swagger security schemes are stale: remove the call to
DocumentBuilder.addBearerAuth(...) and the addApiKey(...) that registers
'x-admin-api-key', and replace them with a single addApiKey(...) entry for the
actual header used by McpV2Controller (name: 'x-api-key', in: 'header',
description: 'API key for MCP v2 access') or drop all auth definitions entirely
if the MCP endpoints are excluded from Swagger via `@ApiExcludeEndpoint`() —
update the DocumentBuilder usage accordingly to reflect only the current
x-api-key scheme.
---
Nitpick comments:
In `@src/main.ts`:
- Around line 12-21: The variable enableCors is computed but never used, so
update the logic around app.enableCors: either remove the unused enableCors
variable and its comment if CORS should always be enabled, or use enableCors to
gate the call to app.enableCors (e.g. if (!enableCors) skip calling
app.enableCors). Locate the computation of enableCors and the
app.enableCors(...) invocation in main.ts and implement the chosen option so the
ENABLE_CORS config actually controls whether app.enableCors is invoked.
In `@src/mcp_v2/controllers/mcp-v2.controller.ts`:
- Line 44: The non-null assertion on apiKey is redundant; update the controller
code that calls this.sessionStore.setApiKey(newSessionId, apiKey!) to pass
apiKey directly (remove the trailing '!') since apiKey is already checked
earlier, e.g., in the method where newSessionId and apiKey are used; keep the
call as this.sessionStore.setApiKey(newSessionId, apiKey) so the types remain
consistent and no needless assertion is present.
- Around line 17-20: The decorators on the MCP v2 controller are contradictory:
`@ApiExcludeEndpoint`() hides the endpoint from Swagger while `@ApiOperation` and
`@ApiResponse` on the same handler are therefore ineffective; decide the intent
and make them consistent—if you want the endpoint documented, remove
`@ApiExcludeEndpoint`() from the controller (retain `@ApiOperation` and
`@ApiResponse`), otherwise remove the `@ApiOperation` and `@ApiResponse` decorators
(keep `@ApiExcludeEndpoint`) so there are no dead annotations; look for the
decorators on the method where `@All`(), `@ApiOperation`, `@ApiResponse`, and
`@ApiExcludeEndpoint` are applied to update accordingly.
- Around line 29-63: The transports Map (this.transports) is process-local so a
returning sessionId may not be found on other instances; update the handler to
avoid silently treating a missing transport as a "new session" and either (A)
require/declare sticky sessions: when sessionId is present but
this.transports.has(sessionId) is false, respond with a clear 410/400 JSON-RPC
error referencing the missing session and instruct the client/operator to use
load-balancer affinity on mcp-session-id, or (B) implement a distributed
session-to-instance registry (reuse sessionStore to persist the owning instance
id when onsessioninitialized in StreamableHTTPServerTransport and, on a missing
transport in transport lookup, proxy/forward the request to the recorded
instance or return a redirect/error). Modify the code paths around sessionId
lookup, the transport creation block, and onsessioninitialized/transport.onclose
to set and remove the instance id in the shared sessionStore so other instances
can detect and handle missing transports.
In `@src/mcp_v2/mcp-v2.module.ts`:
- Around line 9-14: Remove the redundant import of ConfigModule from the MCP V2
module since it is already globally imported in AppModule. Additionally,
consider importing HttpModule with the same configuration used in AppModule
(timeout and maxRedirects) to ensure consistent HttpService settings for
ApiClientV2Service, especially if default Axios settings might be relied on
later.
In `@src/mcp_v2/services/mcp-v2.service.ts`:
- Around line 89-115: Replace the magic numeric command codes used when building
the command array for args.action by extracting them into clearly named
constants (e.g., SWITCH, BRIGHTNESS, KELVIN, TEMPERATURE, MODE) and then use
those constants when constructing the command arrays (replace occurrences of 1,
28, 29, 20, 17 with the corresponding named constants); update the switch
handling of args.action and the command variable to reference these constants so
the intent is clear and maintainability improved.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (22)
src/admin/admin.controller.tssrc/admin/admin.module.tssrc/admin/dto/update-config.dto.tssrc/admin/guards/admin-auth.guard.tssrc/api/api.module.tssrc/api/controllers/mcp.controller.tssrc/app.module.tssrc/auth/auth.controller.tssrc/auth/auth.module.tssrc/auth/auth.service.tssrc/main.tssrc/mcp/mcp.module.tssrc/mcp/services/api-client.service.tssrc/mcp/services/mcp.service.tssrc/mcp/services/redis.service.tssrc/mcp/types/mcp.types.tssrc/mcp_v2/controllers/mcp-v2.controller.tssrc/mcp_v2/mcp-v2.module.tssrc/mcp_v2/services/api-client-v2.service.tssrc/mcp_v2/services/mcp-v2.service.tssrc/mcp_v2/services/session-store.service.tssrc/mcp_v2/tools/tools-list-v2.ts
💤 Files with no reviewable changes (14)
- src/admin/dto/update-config.dto.ts
- src/auth/auth.controller.ts
- src/admin/admin.controller.ts
- src/mcp/services/mcp.service.ts
- src/mcp/mcp.module.ts
- src/api/api.module.ts
- src/auth/auth.service.ts
- src/admin/guards/admin-auth.guard.ts
- src/admin/admin.module.ts
- src/api/controllers/mcp.controller.ts
- src/mcp/services/redis.service.ts
- src/mcp/services/api-client.service.ts
- src/auth/auth.module.ts
- src/mcp/types/mcp.types.ts
| @ApiTags('MCP Protocol V2') | ||
| @Controller('mcp') | ||
| export class McpV2Controller { | ||
| private readonly transports = new Map<string, StreamableHTTPServerTransport>(); |
There was a problem hiding this comment.
In-memory transport Map can leak if sessions are never properly closed.
If a client disconnects without triggering onclose (network drop, crash, etc.), the transport and its associated McpServer remain in the Map indefinitely. Over time this causes unbounded memory growth.
Consider adding a periodic cleanup sweep that evicts transports whose sessions have expired in Redis (the TTL is already tracked there), or set a max-age on entries in the Map.
Also applies to: 49-56
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/mcp_v2/controllers/mcp-v2.controller.ts` at line 12, The in-memory
transports Map (transports: Map<string, StreamableHTTPServerTransport>) can leak
if sessions never trigger onclose; add a periodic cleanup task that sweeps the
Map and evicts stale entries: for each key, check the session TTL/state in Redis
(the same session key used by McpServer) and remove/close the
StreamableHTTPServerTransport and its McpServer when the Redis TTL indicates
expiry or the session no longer exists, or alternatively attach a max-age
timestamp to each Map entry and remove entries older than that age; ensure
cleanup runs on a timer (e.g., setInterval) and properly calls
transport.close()/server.shutdown() to release resources.
| } catch (error) { | ||
| const axiosError = error as AxiosError; | ||
| this.logger.error(`API Error [${method} ${path}]:`, axiosError.response?.data || axiosError.message); | ||
| throw axiosError.response?.data || axiosError; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Error propagation loses HTTP status and stack trace.
throw axiosError.response?.data || axiosError discards the HTTP status code and original stack trace. Callers (the MCP tool handlers) have no way to distinguish a 401 (bad API key) from a 404 or 500, making error handling in McpV2Service imprecise.
Consider wrapping the error in a structured object that preserves status and context:
Suggested fix
} catch (error) {
const axiosError = error as AxiosError;
this.logger.error(`API Error [${method} ${path}]:`, axiosError.response?.data || axiosError.message);
- throw axiosError.response?.data || axiosError;
+ throw {
+ status: axiosError.response?.status,
+ message: axiosError.message,
+ data: axiosError.response?.data,
+ };
}Based on learnings, API client errors should throw structured error objects with status, message, context, and details properties.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/mcp_v2/services/api-client-v2.service.ts` around lines 57 - 61, The catch
block in ApiClientV2Service currently throws axiosError.response?.data or the
raw axiosError, which loses HTTP status and original stack; update the catch in
the method handling requests (the catch that casts to AxiosError) to construct
and throw a structured error object (or a custom Error subclass) that includes
status: axiosError.response?.status, message: axiosError.response?.data?.message
|| axiosError.message, context: { method, path }, details:
axiosError.response?.data, and preserve the original error via an originalError
or cause property so the stack trace remains available; ensure callers in
McpV2Service can inspect .status and .details instead of relying on response
data alone.
| server.registerTool('find_user_id', ToolsListV2.find_user_id, async ({ data }, extra) => { | ||
| const sessionId = extra?.sessionId; | ||
| if (!sessionId) throw new Error('Missing sessionId'); | ||
|
|
||
| const apiKey = await this.sessionStore.getApiKey(sessionId); | ||
| if (!apiKey) throw new Error('Missing session API key'); | ||
|
|
||
| // Call the upstream API to find user id by email/phone | ||
| const resp: any = await this.apiClient.post('/api/v2.0/iot-core/user/findUserId', apiKey, { data }); | ||
| const userId = resp?.userId || resp?.user_id || resp?.data?.userId; | ||
| if (!userId) throw new Error('UserId not found'); | ||
|
|
||
| return { content: [{ type: 'text', text: JSON.stringify({ userId }) }] }; | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Duplicate userId resolution logic — extract a helper.
The pattern of resolving userId from data via the findUserId API call is repeated across all three tool handlers (Lines 36-37, 53-54, 74-75), including the same fallback chain (resp?.userId || resp?.user_id || resp?.data?.userId). This violates DRY and makes the response-shape fallbacks harder to maintain.
Suggested refactor — extract a private helper
+ private async resolveUserId(apiKey: string, data: string): Promise<string> {
+ const resp: any = await this.apiClient.post('/api/v2.0/iot-core/user/findUserId', apiKey, { data });
+ const userId = resp?.userId || resp?.user_id || resp?.data?.userId;
+ if (!userId) throw new Error('UserId not found');
+ return userId;
+ }Then replace the inline calls in each handler:
- const resp: any = await this.apiClient.post('/api/v2.0/iot-core/user/findUserId', apiKey, { data });
- const userId = resp?.userId || resp?.user_id || resp?.data?.userId;
- if (!userId) throw new Error('UserId not found');
+ const userId = await this.resolveUserId(apiKey, data);Also applies to: 44-62, 65-78
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/mcp_v2/services/mcp-v2.service.ts` around lines 28 - 41, Extract the
repeated userId resolution into a private helper (e.g.,
resolveUserIdFromFindApi) that accepts apiKey and payload/data, calls
this.apiClient.post('/api/v2.0/iot-core/user/findUserId', apiKey, { data }), and
returns the normalized userId by applying the fallback chain (resp?.userId ||
resp?.user_id || resp?.data?.userId) or throws a clear error; then replace the
inline logic inside each server.registerTool handlers (ToolsListV2.find_user_id
and the other two handlers) to call this.resolveUserIdFromFindApi(sessionApiKey,
data) after obtaining sessionId and apiKey from this.sessionStore.getApiKey, and
return the existing { content: [...] } using the helper result.
| this.client = new Redis({ host, port, password, db }); | ||
| await new Promise<void>((resolve, reject) => { | ||
| this.client.once('ready', () => resolve()); | ||
| this.client.once('error', (e) => reject(e)); | ||
| setTimeout(() => reject(new Error('Redis connect timeout')), 5000); | ||
| }); |
There was a problem hiding this comment.
Redis client lacks retry strategy and ongoing error handling.
Compared to the existing src/mcp/services/redis.service.ts, this Redis client has no retryStrategy or maxRetriesPerRequest, and no listener for post-init errors. If Redis becomes temporarily unavailable after startup:
- ioredis will buffer commands by default, potentially causing memory buildup.
- Unhandled
'error'events after theonce('error')fires during init can crash the process.
Consider adding a retry strategy and a persistent error listener:
Suggested improvement
- this.client = new Redis({ host, port, password, db });
+ this.client = new Redis({
+ host,
+ port,
+ password,
+ db,
+ retryStrategy: (times) => Math.min(times * 50, 2000),
+ maxRetriesPerRequest: 3,
+ });
+ this.client.on('error', (err) => {
+ this.logger.error('Redis error', err.message);
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| this.client = new Redis({ host, port, password, db }); | |
| await new Promise<void>((resolve, reject) => { | |
| this.client.once('ready', () => resolve()); | |
| this.client.once('error', (e) => reject(e)); | |
| setTimeout(() => reject(new Error('Redis connect timeout')), 5000); | |
| }); | |
| this.client = new Redis({ | |
| host, | |
| port, | |
| password, | |
| db, | |
| retryStrategy: (times) => Math.min(times * 50, 2000), | |
| maxRetriesPerRequest: 3, | |
| }); | |
| this.client.on('error', (err) => { | |
| this.logger.error('Redis error', err.message); | |
| }); | |
| await new Promise<void>((resolve, reject) => { | |
| this.client.once('ready', () => resolve()); | |
| this.client.once('error', (e) => reject(e)); | |
| setTimeout(() => reject(new Error('Redis connect timeout')), 5000); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/mcp_v2/services/session-store.service.ts` around lines 22 - 27, The Redis
client initialization in the constructor uses this.client = new Redis(...) and
only one-time 'ready'/'error' listeners, so it needs a retryStrategy,
maxRetriesPerRequest and a persistent error listener to avoid unbounded
buffering and unhandled post-init errors; update the Redis options passed to new
Redis(...) to include a retryStrategy (mirror the logic from
src/mcp/services/redis.service.ts), set maxRetriesPerRequest to a sane value
(and consider enableOfflineQueue=false if appropriate), and replace the one-time
'error' listener with a persistent this.client.on('error', handler) that
logs/handles errors after init while keeping the existing init Promise
resolution using once('ready')/once('error').
| async onModuleDestroy() { | ||
| await this.client.quit(); | ||
| } |
There was a problem hiding this comment.
Guard onModuleDestroy against uninitialized client.
If onModuleInit fails (e.g., Redis timeout), this.client may be partially initialized. Calling .quit() on it could throw.
Suggested fix
async onModuleDestroy() {
- await this.client.quit();
+ await this.client?.quit();
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async onModuleDestroy() { | |
| await this.client.quit(); | |
| } | |
| async onModuleDestroy() { | |
| await this.client?.quit(); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/mcp_v2/services/session-store.service.ts` around lines 31 - 33, Guard the
onModuleDestroy method so it safely handles a partially-initialized Redis
client: check that this.client exists and is in a state that supports quit
(e.g., non-null and not already closed) before calling this.client.quit(), and
wrap the quit call in a try/catch to swallow or log any errors; update the
onModuleDestroy implementation to reference the existing onModuleInit/client
initialization flow and ensure no unguarded .quit() calls occur if onModuleInit
failed.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/mcp_v2/services/mcp-v2.service.ts (1)
90-92: Duplicate userId resolution logic — already flagged in a prior review.The inline
findUserIdcall + multi-shape fallback (r?.userId || r?.user_id || r?.data?.userId) is repeated here (lines 90-92) and again at lines 110-112, identical to the pattern already raised in the previous review forfind_user_id. The prior suggestion to extract aprivate resolveUserId(apiKey, data)helper still applies.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/mcp_v2/services/mcp-v2.service.ts` around lines 90 - 92, Extract a private helper method, e.g. private async resolveUserId(apiKey, data): Promise<string>, that performs the post to '/api/v2.0/iot-core/user/findUserId' via this.apiClient.post(apiPath, apiKey, { data }), normalizes the response into a single uid using the existing shape checks (r?.userId || r?.user_id || r?.data?.userId) and throws the same Error('UserId not found') when missing; then replace the duplicated inline blocks in this service with calls to this.resolveUserId(apiKey, data) to centralize logic and remove the repeated fallback code.
🧹 Nitpick comments (2)
src/mcp_v2/services/mcp-v2.service.ts (2)
56-66:init_api_keythrows raw errors while every other tool returns structured{ isError: true }objects.All other handlers are wrapped by
withApiKey, which catches thrown errors and maps them to{ isError: true, content: [...] }.init_api_keysits outside that wrapper and throws directly. If the MCP SDK's internal handler dispatch doesn't catch and re-wrap thrown errors the same way, the client will receive an unstructured error response. Consider wrapping the body in a try/catch that mirrors thewithApiKeyconvention:♻️ Proposed fix — consistent error-response shape
- server.registerTool('init_api_key', ToolsListV2.init_api_key, async (args: any, extra: any) => { - const apiKey = args?.apiKey; - const sessionId = extra?.sessionId; - if (!sessionId) throw new Error('Missing sessionId'); - if (!apiKey) throw new Error('apiKey is required'); - - await this.sessionStore.setApiKey(sessionId, apiKey); - this.logger.log(`Initialized apiKey for session ${sessionId}`); - - return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: 'apiKey initialized for session' }) }] } as any; - }); + server.registerTool('init_api_key', ToolsListV2.init_api_key, async (args: any, extra: any) => { + try { + const apiKey = args?.apiKey; + const sessionId = extra?.sessionId; + if (!sessionId) return { isError: true, content: [{ type: 'text', text: 'Missing sessionId in request' }] } as any; + if (!apiKey) return { isError: true, content: [{ type: 'text', text: 'apiKey is required' }] } as any; + + await this.sessionStore.setApiKey(sessionId, apiKey); + this.logger.log(`Initialized apiKey for session ${sessionId}`); + + return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: 'apiKey initialized for session' }) }] } as any; + } catch (err: any) { + this.logger.error(`init_api_key error: ${err?.message || err}`, err?.stack); + return { isError: true, content: [{ type: 'text', text: `Error: ${err?.message || err}` }] } as any; + } + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/mcp_v2/services/mcp-v2.service.ts` around lines 56 - 66, The init_api_key tool handler currently throws raw Errors (e.g., "Missing sessionId", "apiKey is required") instead of returning the structured error object used elsewhere; update the async handler passed to server.registerTool('init_api_key', ToolsListV2.init_api_key, ...) to wrap its body in try/catch, call this.sessionStore.setApiKey(...) and this.logger.log(...) in the try, and in catch return the same shaped response used by withApiKey (an object with isError: true and content: [...] containing the error message) so clients always receive a consistent error-response shape.
50-52:withApiKeycatch block logs nothing server-side — errors are silently swallowed.Any exception thrown inside a
withApiKey-wrapped handler (including upstream API failures, programming errors, and thrown validation errors) is caught here and returned to the client as a text message, but never logged withthis.logger. This makes production debugging very difficult.♻️ Proposed fix — add server-side error logging
} catch (err: any) { + this.logger.error(`Tool handler error: ${err?.message || err}`, err?.stack); return { isError: true, content: [{ type: 'text', text: `Error: ${err?.message || err}` }] } as any; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/mcp_v2/services/mcp-v2.service.ts` around lines 50 - 52, The catch in the withApiKey-wrapped handler currently returns the error to the client but never logs it; update the catch block inside withApiKey (in mcp-v2.service.ts) to call this.logger.error with the error and its stack/metadata (e.g., this.logger.error('withApiKey handler error', err)) before returning the existing { isError... } response so server-side exceptions are recorded for debugging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/mcp_v2/services/mcp-v2.service.ts`:
- Around line 83-99: The handler registered for 'list_devices' destructures {
userId, data } but ToolsListV2.list_devices.inputSchema does not include data,
so the SDK strips it and the fallback branch that resolves email/phone is dead;
either add a data field to the list_devices inputSchema in tools-list-v2.ts
(e.g., include data: z.any()/object similar to control_device_simple) so the
handler can receive and use data, or remove the unreachable fallback code in the
server.registerTool(...) handler in mcp-v2.service.ts (the block that calls
this.apiClient.post('/api/v2.0/iot-core/user/findUserId', ...) and checks for
uid) and rely solely on userId being required/validated.
---
Duplicate comments:
In `@src/mcp_v2/services/mcp-v2.service.ts`:
- Around line 90-92: Extract a private helper method, e.g. private async
resolveUserId(apiKey, data): Promise<string>, that performs the post to
'/api/v2.0/iot-core/user/findUserId' via this.apiClient.post(apiPath, apiKey, {
data }), normalizes the response into a single uid using the existing shape
checks (r?.userId || r?.user_id || r?.data?.userId) and throws the same
Error('UserId not found') when missing; then replace the duplicated inline
blocks in this service with calls to this.resolveUserId(apiKey, data) to
centralize logic and remove the repeated fallback code.
---
Nitpick comments:
In `@src/mcp_v2/services/mcp-v2.service.ts`:
- Around line 56-66: The init_api_key tool handler currently throws raw Errors
(e.g., "Missing sessionId", "apiKey is required") instead of returning the
structured error object used elsewhere; update the async handler passed to
server.registerTool('init_api_key', ToolsListV2.init_api_key, ...) to wrap its
body in try/catch, call this.sessionStore.setApiKey(...) and
this.logger.log(...) in the try, and in catch return the same shaped response
used by withApiKey (an object with isError: true and content: [...] containing
the error message) so clients always receive a consistent error-response shape.
- Around line 50-52: The catch in the withApiKey-wrapped handler currently
returns the error to the client but never logs it; update the catch block inside
withApiKey (in mcp-v2.service.ts) to call this.logger.error with the error and
its stack/metadata (e.g., this.logger.error('withApiKey handler error', err))
before returning the existing { isError... } response so server-side exceptions
are recorded for debugging.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/mcp_v2/controllers/mcp-v2.controller.tssrc/mcp_v2/services/mcp-v2.service.tssrc/mcp_v2/tools/tools-list-v2.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/mcp_v2/tools/tools-list-v2.ts
- src/mcp_v2/controllers/mcp-v2.controller.ts
| server.registerTool( | ||
| 'list_devices', | ||
| ToolsListV2.list_devices, | ||
| withApiKey(async ({ userId, data }, apiKey) => { | ||
| // If admin provided email/phone in `data`, resolve it first | ||
| let uid = userId; | ||
| if (!uid && data) { | ||
| const r: any = await this.apiClient.post('/api/v2.0/iot-core/user/findUserId', apiKey, { data }); | ||
| uid = r?.userId || r?.user_id || r?.data?.userId; | ||
| if (!uid) throw new Error('UserId not found'); | ||
| } | ||
|
|
||
| if (!uid) throw new Error('userId is required'); | ||
|
|
||
| const devices: any = await this.apiClient.get(`/device/${uid}`, apiKey); | ||
| return { content: [{ type: 'text', text: JSON.stringify({ total: Array.isArray(devices) ? devices.length : 0, devices }) }] } as any; | ||
| }), |
There was a problem hiding this comment.
data is absent from ToolsListV2.list_devices's schema — the email/phone fallback path is dead code.
The handler destructures { userId, data } from args, but the input schema for list_devices only defines userId (via .partial()), with no data field:
// tools-list-v2.ts
list_devices: {
inputSchema: z.object({ userId: z.string().describe('End-user userId') }).partial(),
},The SDK validates and strips inputs against the registered schema, so data will always be undefined here. The fallback branch on lines 89-93 can never execute, and any call that omits userId will unconditionally hit the throw new Error('userId is required') on line 95.
Either add data to the schema (like control_device_simple does) or remove the dead fallback:
🐛 Option A — add `data` to the `list_devices` schema in tools-list-v2.ts
list_devices: {
description: 'List devices for a given end-user `userId`.',
- inputSchema: z.object({ userId: z.string().describe('End-user userId') }).partial(),
+ inputSchema: z.object({
+ userId: z.string().describe('End-user userId').optional(),
+ data: z.string().optional().describe('End-user email or phone (resolved if userId absent)'),
+ }),
},🐛 Option B — remove the unreachable fallback in the handler
- withApiKey(async ({ userId, data }, apiKey) => {
- // If admin provided email/phone in `data`, resolve it first
- let uid = userId;
- if (!uid && data) {
- const r: any = await this.apiClient.post('/api/v2.0/iot-core/user/findUserId', apiKey, { data });
- uid = r?.userId || r?.user_id || r?.data?.userId;
- if (!uid) throw new Error('UserId not found');
- }
-
- if (!uid) throw new Error('userId is required');
+ withApiKey(async ({ userId }, apiKey) => {
+ if (!userId) throw new Error('userId is required');
+ const uid = userId;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/mcp_v2/services/mcp-v2.service.ts` around lines 83 - 99, The handler
registered for 'list_devices' destructures { userId, data } but
ToolsListV2.list_devices.inputSchema does not include data, so the SDK strips it
and the fallback branch that resolves email/phone is dead; either add a data
field to the list_devices inputSchema in tools-list-v2.ts (e.g., include data:
z.any()/object similar to control_device_simple) so the handler can receive and
use data, or remove the unreachable fallback code in the
server.registerTool(...) handler in mcp-v2.service.ts (the block that calls
this.apiClient.post('/api/v2.0/iot-core/user/findUserId', ...) and checks for
uid) and rely solely on userId being required/validated.
This reverts commit ea08ba4.
Summary by CodeRabbit
Breaking Changes
New Features
Architecture Updates